﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Red.EngineData.Protocol.Interop;

namespace Red.EngineData.View
{
    /// <summary>
    /// Fake children implementation for array
    /// </summary>
    public class ArrayChildList : IList<string>
    {
        private int _count;

        public ArrayChildList( int count )
        {
            _count = count;
        }

        #region Implementation of IEnumerable

        public IEnumerator<string> GetEnumerator()
        {
            for ( int i = 0; i < _count; ++i )
            {
                yield return this[i];
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion

        #region Implementation of ICollection<T>

        public int Count { get { return _count; } }
        public bool IsReadOnly { get { return true; } }

        public void Add( string item ) { }
        public void Clear() { }
        public bool Contains( string item )
        {
            int index = -1;
            if ( int.TryParse( item, out index ) == false )
                return false;
            return ( index >= 0 && index < _count );
        }
        public void CopyTo( string[] array, int arrayIndex ) { }
        public bool Remove( string item ) { return false; }

        #endregion

        #region Implementation of IList<T>

        public string this[int index]
        {
            get
            {
                return string.Format( "[{0}]", index );
            }
            set { }
        }

        public int IndexOf( string item ) { return 0; }
        public void Insert( int index, string item ) { }
        public void RemoveAt( int index ) { }

        #endregion
    };

    /// <summary>
    /// Abstract data view. It can be object, preview window, scene, whole editor or top most view (root view).
    /// </summary>
    public sealed class DataView : IEquatable<DataView>
    {
        // Constance properties
        private readonly string _path;
        private readonly string _interopName;

        // Member fields
        private string _type = "unknown";
        private string _data = "unknown";
        private EInteropType _interopType = EInteropType.Value;
        private IList<string> _children = new List<string>();
        private IList<string> _categories = new List<string>();
        private bool _isReadOnly = false;

        // Used to schedule incoming actions, provides thread-safety
        private InteropProtocol _interopProtocol;
        private DataViewFactory _factory;

        /// <summary>
        /// Name used to identify object in identification path. For array elements it may be "[n]", where n is a index in array.
        /// </summary>
        public string InteropName
        {
            get { return _interopName; }
        }

        /// <summary>
        /// Type identifier of the data
        /// </summary>
        public string Type
        {
            get { return _type; }
        }

        /// <summary>
        /// Is this view read only.
        /// </summary>
        public bool IsReadOnly
        {
            get { return _isReadOnly; }
        }

        /// <summary>
        /// Value of this view.
        /// </summary>
        public string Value
        {
            get { return _data; }

            set
            {
                _data = value;
                ScheduleSetValueAction( value, null, null );
            }
        }

        /// <summary>
        /// Sub-views (properties) of this view.
        /// </summary>
        public IList<string> Children
        {
            get { return _children; }
        }

        /// <summary>
        /// Category for each child
        /// </summary>
        public IList<string> Categories
        {
            get { return _categories; }
        }

        /// <summary>
        /// Path to the data view (i.e. "obj.vector.x").
        /// </summary>
        public string Path
        {
            get { return _path; }
        }

        /// <summary>
        /// Does this data view contains sub-views (even if they are not listed in children.
        /// </summary>
        public bool IsCompound
        {
            get { return _interopType == EInteropType.Compound; }
        }

        /// <summary>
        /// Is this data view an array.
        /// </summary>
        public bool IsArray
        {
            get { return _interopType == EInteropType.Array; }
        }

        /// <summary>
        /// Is this data view a functor.
        /// </summary>
        public bool IsCallable
        {
            get { return _interopType == EInteropType.Functor; }
        }

        /// <summary>
        /// Is this data view a simple value.
        /// </summary>
        public bool IsSimpleValue
        {
            get { return _interopType == EInteropType.Value; }
        }

        /// <summary>
        /// Is this data view a simple value.
        /// </summary>
        public bool IsNotifier
        {
            get { return _interopType == EInteropType.Notifier; }
        }

        /// <summary>
        /// Creates uninitialized DataView for root. That means that view will be initialized later by explicit Refresh call.
        /// </summary>
        internal DataView( InteropProtocol actionProtocol, DataViewFactory dataViewFactory )
        {
            // Properties which never change
            _interopName = "";
            _path = "";
            _interopProtocol = actionProtocol;
            _factory = dataViewFactory;
        }

        /// <summary>
        /// Creates uninitialized DataView. That means that view will be initialized later by explicit Refresh call.
        /// </summary>
        internal DataView( string path, InteropProtocol actionProtocol, DataViewFactory dataViewFactory )
        {
            // Properties which never change
            _interopName = FindNameInPath( path );
            _path = path;
            _interopProtocol = actionProtocol;
            _factory = dataViewFactory;
        }

        /// <summary>
        /// Find last interop name in path.
        /// </summary>
        private static string FindNameInPath( string path )
        {
            if ( path.Length == 0 )
                return "";

            int index = FirstNameSpecialCharacterIndex( path, true );

            if ( index == -1 )
            {
                return path;
            }

            // Skip the dot
            if ( path[index] == '.' )
                index++;

            string result = path.Substring( index, path.Length - index );
            return result;
        }

        /// <summary>
        /// Helper method for finding special name separator character.
        /// </summary>
        private static int FirstNameSpecialCharacterIndex( string value, bool reverse )
        {
            if ( reverse )
            {
                for ( int i = value.Length - 1; i > 0; --i )
                {
                    char chr = value[i];
                    if ( chr == '.' || chr == '[' )
                        return i;
                }
            }
            else
            {
                for ( int i = 0; i < value.Length; ++i )
                {
                    char chr = value[i];
                    if ( chr == '.' || chr == '[' )
                        return i;
                }
            }

            return -1;
        }

        public void HasChild( string childName, Action hasChildCallback, Action notHasChildCallback = null )
        {
            const bool ignoreInteropDebugger = true;
            ScheduleAction( new ActionWrapper( PrepareFinalPath( childName ),
                result =>
                {
                    if ( hasChildCallback != null )
                    {
                        hasChildCallback();
                    }
                },
                ex =>
                {
                    if ( notHasChildCallback != null )
                    {
                        notHasChildCallback();
                    }
                }, ignoreInteropDebugger ) );
        }

        /// <summary>
        /// Schedules expression to to send to engine and call proper callback after response
        /// </summary>
        /// <param name="expression">Interop expression</param>
        /// <param name="onResponse">Correct response callback</param>
        /// <param name="onException">Exception response callback</param>
        public void Execute( Interop interopExpression, Action<InteropResult> onResponse = null, Action<ProtocolException> onException = null )
        {
            ScheduleAction( new ActionWrapper( PrepareFinalPath( interopExpression ), onResponse, onException ) );
        }

        /// <summary>
        /// Schedules expression to send to engine and return task
        /// </summary>
        /// <param name="expression">Interop expression</param>
        /// <returns></returns>
        public Task ExecuteAsync( Interop interopExpression )
        {
            var tcs = new TaskCompletionSource<bool>();
            Execute( interopExpression, res => tcs.SetResult( true ), ex => { tcs.SetException( ex ); } );
            return tcs.Task;
        }

        /// <summary>
        /// Schedules expression to send to engine and return task
        /// </summary>
        /// <typeparam name="R">Result type</typeparam>
        /// <param name="expression">Interop expression</param>
        /// <returns></returns>
        public Task<T> ExecuteAsync<T>( Interop interopExpression ) where T : IConvertible
        {
            var tcs = new TaskCompletionSource<T>();
            Execute( interopExpression, res => GetResult( res.Data, tcs ), ex => { tcs.SetException( ex ); } );
            return tcs.Task;
        }

        /// <summary>
        /// Schedules action which gets information about this data view from engine (value, member fields, flags).
        /// Refreshes cached data and calls appropriate actions when done.
        /// </summary>
        /// <param name="onRefreshed">Callback with potential exception</param>
        public void Refresh( Action<ProtocolException> onResponse )
        {
            ScheduleRefreshAction( onResponse );
        }

        /// <summary>
        /// Awaitable version of <see cref="Refresh(Action<ProtocolException>)"/>
        /// </summary>
        /// <returns>Awaitable task giving possible exception or null on success</returns>
        public Task<ProtocolException> Refresh()
        {
            var result = new TaskCompletionSource<ProtocolException>();
            ScheduleRefreshAction( ex => result.SetResult( ex ) );
            return result.Task;
        }

        /// <summary>
        /// Starts the asynchronous process of setting a value and calls appropriate action when finished.
        /// </summary>
        public void SetValue( string value, Action onSet, Action<ProtocolException> onException )
        {
            _data = value;
            ScheduleSetValueAction( value, onSet, onException );
        }

        /// <summary>
        /// Get sub-view down in the hierarchy
        /// </summary>
        /// <param name="relativePath"></param>
        /// <returns></returns>
        public DataView GetSubView( string relativePath )
        {
            return _factory.CreateView( CombinePath( _path, relativePath ) );
        }

        /// <summary>
        /// Request named notifier for this data view.
        /// </summary>
        /// <param name="relativePath">Notifier's name</param>
        /// <param name="onSuccess">Callback if notifier exists in engine and is binded between editor and engine.</param>
        /// <param name="onException">Callback if notifier not exist or happend something else wrong.</param>
        public void RequestNotifier( string relativePath, Action<Notifier> onSuccess, Action<ProtocolException> onException = null )
        {
            ScheduleRequestNotifierAction( relativePath, onSuccess, onException );
        }

        #region Equality

        public bool Equals( DataView other )
        {
            if ( other == null )
            {
                return false;
            }

            return string.Equals( Path, other.Path );
        }

        public override bool Equals( object obj )
        {
            if ( ReferenceEquals( this, obj ) )
            {
                return true;
            }

            if ( ReferenceEquals( null, obj ) || obj.GetType() != GetType() )
            {
                return false;
            }

            return Equals( obj as DataView );
        }

        public override int GetHashCode()
        {
            return string.IsNullOrEmpty( Path ) ? Path.GetHashCode() : 0;
        }

        #endregion

        /// <summary>
        /// Helper function to combine two Interop path parts into one
        /// </summary>
        public static string CombinePath( string p1, string p2 )
        {
            if ( p1.Length == 0 )
                return p2;

            if ( p2.Length == 0 )
                return p1;

            if ( p1.EndsWith( "." ) )
                p1 = p1.Substring( 0, p1.Length - 1 );

            if ( p2.StartsWith( "." ) )
                p2 = p2.Substring( 1 );

            if ( p2.StartsWith( "[" ) )
                return p1 + p2;

            if ( p2.Trim().StartsWith( "=" ) )
                return p1 + p2;

            return p1 + "." + p2;
        }

        /// <summary>
        /// Helper function to split up the given path into its first segment and the rest. 
        /// </summary>
        /// <param name="path">Path to split</param>
        /// <param name="theRest">Remaining path after removing the first segment</param>
        /// <returns>First segment of the path</returns>
        public static string ExtractPathSegment( string path, out string theRest )
        {
            if ( string.IsNullOrEmpty( path ) )
            {
                theRest = string.Empty;
                return string.Empty;
            }
            else
            {
                int idx = path[0] == '[' ? path.IndexOf( ']' ) : FirstNameSpecialCharacterIndex( path, false );
                if ( idx <= 0 )
                {
                    theRest = string.Empty;
                    return path;
                }
                else
                {
                    if ( path[idx] == ']' ) ++idx;
                    theRest = path.Substring( idx < path.Length - 1 && path[idx] == '.' ? idx + 1 : idx );
                    return path.Substring( 0, idx );
                }
            }
        }

        /// <summary>
        /// Creates and schedules set value action.
        /// </summary>
        /// <param name="value"></param>
        private void ScheduleSetValueAction( string value, Action onSet, Action<ProtocolException> onException )
        {
            Execute( Interop.Make( " = {0}", _data ),
                result =>
                {
                    _data = result.Data;
                    if ( onSet != null )
                        onSet();
                },
                ex =>
                {
                    if ( onException != null )
                        onException( ex );
                    else
                        throw ex;
                } );
        }

        /// <summary>
        /// Creates and schedules refresh action.
        /// </summary>
        /// <param name="onRefreshed">Callback with potential exception</param>
        private void ScheduleRefreshAction( Action<ProtocolException> onResponse )
        {
            Execute( Interop.Make( "" ),
                result =>
                {
                    _data = result.Data;
                    _type = result.Type;
                    _interopType = result.InteropType;
                    _isReadOnly = result.IsReadOnly;
                    _categories = result.Categories;

                    if ( _interopType == EInteropType.Array )
                    {
                        int arrayCount = 0;
                        if ( int.TryParse( result.Data.Trim(), out arrayCount ) == false )
                        {
                            ProtocolException ex = new ProtocolException( "String with array size has incorrect format", null );
                            if ( onResponse != null )
                                onResponse( ex );
                            else
                                throw ex;
                        }

                        // because children and categories for array are not sent by engine
                        _children = new ArrayChildList( arrayCount );
                        _categories = new List<string>( new string[arrayCount] );
                    }
                    else
                    {
                        _children = new List<string>();

                        foreach ( var child in result.Children )
                        {
                            if ( child.StartsWith( "_" ) )
                                continue;
                            else
                                _children.Add( child );
                        }
                    }

                    if ( onResponse != null )
                        onResponse( null );
                },
                ex =>
                {
                    if ( onResponse != null )
                        onResponse( ex );
                    else
                        throw ex;
                } );
        }

        private void ScheduleRequestNotifierAction( string relativePath, Action<Notifier> onSuccess, Action<ProtocolException> onException )
        {
            Execute( Interop.Make( relativePath ),
                result =>
                {
                    uint notifierId = 0;
                    if ( uint.TryParse( result.Data, out notifierId ) )
                    {
                        if ( onSuccess != null )
                        {
                            Notifier newNotifier = _factory.CreateNotifier( notifierId );
                            onSuccess( newNotifier );
                        }
                    }
                    else
                    {
                        if ( onException != null )
                            onException( new ProtocolException( "Incorrect notifier id format!", null ) );
                    }
                },
                ex =>
                {
                    if ( onException != null )
                        onException( ex );
                } );
        }

        /// <summary>
        /// Helper method for action scheduling.
        /// </summary>
        /// <param name="action"></param>
        private void ScheduleAction( ActionWrapper action )
        {
            _interopProtocol.Execute( action );
        }

        /// <summary>
        /// Helper method.
        /// </summary>
        private void GetResult<T>( string res, TaskCompletionSource<T> tcs ) where T : IConvertible
        {
            try
            {
                T val = (T)Convert.ChangeType( res, typeof( T ) );
                tcs.SetResult( val );
            }
            catch ( System.Exception ex )
            {
                tcs.SetException( ex );
            }
        }

        /// <summary>
        /// Prepares final interop path combines parent path and expression
        /// </summary>
        /// <param name="expression">Interop expression called on parent object</param>
        /// <returns>Full final interop path</returns>
        private string PrepareFinalPath( string expression )
        {
            return ( Path != "" && expression != "" ) ? CombinePath( Path, expression ) : ( Path != "" ) ? Path : expression;
        }
    }
}
